const Koa = require('koa')
const router = require('koa-router')()
const koaBody = require('koa-body')
const net = require('net')
const uuid = require('uuid')
const YAML = require('yamljs')
const fs = require('fs')
const { createServer } = require('http')
const { Server } = require('socket.io')
const encryptList = ['aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc']
const crypto = require('crypto')
const { uid } = require('./utils/common')

const tokens = {
  admin: {
    token: 'admin-token',
  },
  editor: {
    token: 'editor-token',
  },
}

const users = {
  'admin-token': {
    roles: ['admin'],
    introduction: 'I am a super administrator',
    avatar:
      'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
    name: 'Super Admin',
  },
  'editor-token': {
    roles: ['editor'],
    introduction: 'I am an editor',
    avatar:
      'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
    name: 'Normal Editor',
  },
}

/*  --------------- data structure  -----------------

---------  table: activeSocket  ------------
| persistentConnectionUID | activeSocket |  remote_port  |  local_port



---------------------------  table: request  ----------------------------
| persistentConnectionUID | requestSocket |  remote_port  |  local_port  |  activeSocket



A 80   ------------------|---------------------------- Server 27---|---------81 ------------------RQ1
                                                                                ------------------RQ2
  8080 ------------------|                                         |---------8081
                                                                                ------------------RQ3
                                                                                ------------------RQ4

B 80   ------------------|                                      ---|---------82
  8080 ------------------|                                      ---|---------8082




C 80   ------------------|---------------------------- Server 27---|---------83
  8080 ------------------|                                         |---------8083


D 80   ------------------|                                      ---|---------84
  8080 ------------------|                                      ---|---------8084
*/

class ActiveServer {
  natPort = 3000
  activeIO
  // the main long connection socket
  activeSocketObjs = []
  /* the requests from front client
   * {requestKey: 'UC243232345', socket: socket}
   */
  requests = {}
  server_config = {}
  binds = {}
  isBinded = false
  isConnected = false
  isInited = false
  unids = {}

  constructor(argv) {
    this.init(argv)
  }
  createactiveSocket(options) {
    if (options.port === undefined) {
      console.error('please set a local port for activeSocket!')
      return
    }
    const app = new Koa()
    app.use(koaBody())
    router.post('/vue-admin-template/user/login', async (ctx) => {
      const { username, password } = ctx.request.body
      // console.log(username, password, '------------');

      ctx.body = {
        code: 20000,
        data: tokens['admin'],
      }
      // return ctx.body
    })
    router.get('/vue-admin-template/user/info', async (ctx) => {
      ctx.body = {
        code: 20000,
        data: users['admin-token'],
      }
    })
    router.get('/base-config', async (ctx) => {
      let server_config = YAML.parse(
        fs.readFileSync('./config/test.yaml').toString()
      )
      ctx.body = {
        code: 20000,
        data: Object.assign({}, server_config, { unids: this.unids }),
      }
    })
    router.post('/base-config-Add', async (ctx) => {
      let options = {
        code: 20000,
        status: true,
        msg: '添加成功',
      }
      let server_config = YAML.parse(
        fs.readFileSync('./config/test.yaml').toString()
      )
      let item = ctx.request.body
      server_config.listentPort = item.listentPort
      if (server_config.clients[item.id]) {
        if (server_config.clients[item.id].binds[item.name]) {
          options = {
            code: 20000,
            status: false,
            msg: '不能添加重复配置',
          }
        } else {
          server_config.clients[item.id].binds[item.name] = {
            local_port: item.local_port,
            local_host: item.local_host,
            remote_port: item.remote_port,
          }
        }
      } else {
        server_config.clients[item.id] = { binds: {} }
        server_config.clients[item.id].binds[item.name] = {
          local_port: item.local_port,
          local_host: item.local_host,
          remote_port: item.remote_port,
        }
      }

      fs.writeFileSync('./config/test.yaml', YAML.stringify(server_config))
      ctx.body = options
    })
    router.post('/base-config-edit', async (ctx) => {
      let server_config = YAML.parse(
        fs.readFileSync('./config/test.yaml').toString()
      )
      let item = ctx.request.body
      server_config.listentPort = item.listentPort
      server_config.clients[item.id].binds[item.name] = {
        local_port: item.local_port,
        local_host: item.local_host,
        remote_port: item.remote_port,
      }
      fs.writeFileSync('./config/test.yaml', YAML.stringify(server_config))
      ctx.body = {
        code: 20000,
        msg: '更新成功',
      }
    })
    router.post('/base-config-del', async (ctx) => {
      let server_config = YAML.parse(
        fs.readFileSync('./config/test.yaml').toString()
      )
      let item = ctx.request.body
      delete server_config.clients[item.id].binds[item.name]
      fs.writeFileSync('./config/test.yaml', YAML.stringify(server_config))
      ctx.body = {
        code: 20000,
        msg: '更新成功',
      }
    })
    app.use(router.routes())
    app.use(router.allowedMethods())
    app.listen(8000)
    this.httpServer = createServer(app.callback())
    this.activeIO = new Server(this.httpServer, { pingTimeout: 60000 })
    //http2 赋值
    //this.activeIO=server;

    this.activeIO.engine.generateId = (req) => {
      return uuid.v4() // must be unique across all Socket.IO servers
    }
    this.activeIO.on('connection_error', (err) => {
      console.log(err.req) // the request object
      console.log(err.code) // the error code, for example 1
      console.log(err.message) // the error message, for example "Session ID unknown"
      console.log(err.context) // some additional error context
    })

    try {
      // 服务端监听连接状态：io的connection事件表示客户端与服务端成功建立连接，它接收一个回调函数，回调函数会接收一个socket参数。
      this.activeIO.on('connection', (longSocket) => {
        this.isInited = true
        this.isConnected = true
        console.log('client connect server, ok!')
        const localListeningSockets = []
        const activeObj = { activeSocket: longSocket, localListeningSockets }
        longSocket.emit('server message', 'hello')
        //向客户端发送config信息
        longSocket.emit('base config', this.server_config)
        // 监听断开连接状态：socket的disconnect事件表示客户端与服务端断开连接
        longSocket.on('disconnect', (reason) => {
          this.isConnected = false
          // 1、关闭消费客户端请求
          console.log(`长连断开，原因: ${reason}`)
          console.log('connect disconnect')
          this.activeSocketObjs = []
          for (let key in this.requests) {
            this.requests[key].LANSocket.end()
            this.requests[key] = null
            delete this.requests[key]
          }
          longSocket.disconnect()
          activeObj.activeSocket = null
          delete activeObj['activeSocket']
          this.unids = {}
        })

        longSocket.on('persistentConnectionUID', (data) => {
          const { clients } = this.server_config
          let newActiveObj = Object.assign({}, activeObj)
          Object.keys(clients[data].binds).forEach((key) => {
            this.binds[data + key] = clients[data].binds[key]
          })
          console.log(this.binds)
          // 更新长连
          console.log(`event persistentConnectionUID: ${JSON.stringify(data)}`)
          const unid = this.decrypt(data)
          newActiveObj.unid = unid
          const activeSocketObj = this.getActiveSocketByKey(unid)
          // 先判断对应的长连接是否存在，存在则更新数据，否则添加到集合中
          if (activeSocketObj) {
            activeSocketObj.activeSocket = longSocket
          } else {
            this.activeSocketObjs.push(newActiveObj)
          }

          // if (this.isInited) {
          //   for (let serverKey in this.binds) {
          //     // 监听事件
          //     this.updateLocalListeningSocket(serverKey, unid)
          //   }
          // }
          this.addBands(newActiveObj, data)
          //获取状态和ip地址
          if (!this.unids[data]) {
            this.unids[data] = getIP(longSocket)
          }
        })

        longSocket.on('updateLongSocket', (data) => {
          const unid = this.decrypt(data)
          const activeSocketObj = this.getActiveSocketByKey(unid)
          for (const serverKey in this.binds) {
            activeSocketObj.activeSocket.on(serverKey, (d) => {
              d = this.decrypt(d)
              const { cmd, data, requestKey } = d
              if (!this.requests[requestKey]) {
                return
              }
              if (cmd === 'close') {
                this.requests[requestKey].LANSocket.end()
                this.requests[requestKey] = null
                delete this.requests[requestKey]
              } else if (cmd === 'data') {
                console.log(
                  `send b data: ${Buffer.from(data).toString('utf-8', 0, 100)}`
                )
                this.requests[requestKey].LANSocket.write(data)
              }
            })
          }
        })

        if (this.binds !== undefined && activeObj.unid) {
          console.log(`activeObj.unid: ${activeObj.unid}`)
          this.addReceiveClientDataHandler(activeObj.unid)
        }
        longSocket.on('removeBinds', (data) => {
          data = this.decrypt(data)
          console.log(`remove binds: ${data}`)
          this.handleRemoveBinds(data, localListeningSockets)
        })
        longSocket.on('ping', () => {
          console.log('[E] 心跳请求已发出 →')
          longSocket.volatile.emit('ping')
        })
      })
      return new Promise((resolve, reject) => {
        this.httpServer.listen(options.port, () => {
          console.log(`监听端口: ${options.port}`)
          resolve()
        })
        this.httpServer.on('error', (e) => {
          reject(e)
        })
      })
    } catch (ex) {
      console.error('error from catch: ', ex)
    }
  }
  encrypt(data) {
    if (this.key) {
      const cipher = crypto.createCipheriv(
        this.encryptMethod,
        this.key,
        this.iv
      )
      let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex')
      encrypted += cipher.final('hex')
      return encrypted
    }
    return data
  }
  decrypt(encrypted) {
    if (this.key) {
      const decipher = crypto.createDecipheriv(
        this.encryptMethod,
        this.key,
        this.iv
      )
      let decrypted = decipher.update(encrypted, 'hex', 'utf8')
      decrypted += decipher.final('utf8')
      return JSON.parse(decrypted)
    }
    return encrypted
  }

  deleteActiveSocketByKey(unid) {
    for (let i = 0; i < this.activeSocketObjs.length; i++) {
      if (this.activeSocketObjs[i].unid === unid) {
        this.activeSocketObjs.splice(i, 1)
        i--
      }
    }
  }

  getActiveSocketByKey(unid) {
    if (!unid) {
      console.error('unid 不能为空')
      return
    }
    console.log('getActiveSocketByKey ...')
    console.log(`unid: ${unid}`)
    console.log(`this.activeSocketObjs.length: ${this.activeSocketObjs.length}`)
    console.log(
      `this.activeSocketObjs: ${this.activeSocketObjs.map((item) => item.unid)}`
    )

    return this.activeSocketObjs.find((item) => item.unid === unid)
  }

  getLocalListeningSocketByActiveSocketAndKey(persistentConnectionUID, key) {
    this.printDataStructure()
    const activeSocketObj = this.activeSocketObjs.find(
      (item) => item.unid === persistentConnectionUID
    )
    console.log(`activeSocketObj: ${activeSocketObj}`)
    if (activeSocketObj) {
      console.log(
        `activeSocketObj.localListeningSockets[key]: ${activeSocketObj.localListeningSockets[key]}`
      )
      return activeSocketObj.localListeningSockets[key]
    }
  }

  addBands(activeObj, unid) {
    for (let key in this.binds) {
      if (
        !this.getLocalListeningSocketByActiveSocketAndKey(activeObj.unid, key)
      ) {
        this.createLocalListeningSocket(
          key,
          {
            port: this.binds[key].remote_port,
            host: this.binds[key].local_host,
          },
          activeObj.unid
        )
      }
    }
  }

  // 关闭转发服务的监听端口
  /**
   * param option
   * {
      local_port: 8090,
      local_host: '127.0.0.1',
      remote_port: 9080
    },
   */
  closeLocalListeningSockets(activeObj) {
    for (let i = 0; i < this.activeSocketObjs.length; i++) {
      if (this.activeSocketObjs[i] === activeObj) {
        this.activeSocketObjs[i].localListeningSockets.forEach(
          (localListeningSocket) => {
            localListeningSocket.end()
          }
        )
        this.activeSocketObjs.splice(i, 1)
        break
      }
    }
  }
  // 处理来自客户端的数据
  addReceiveClientDataHandler(unid) {
    if (!unid) {
      throw `unid不能为空666`
    }
    const activeSocketObj = this.getActiveSocketByKey(unid)
    for (let serverKey in this.binds) {
      activeSocketObj.activeSocket.on(serverKey, (d) => {
        d = this.decrypt(d)
        const { cmd, data, requestKey } = d
        console.log(
          `receive c data: ${Buffer.from(data || '').toString('utf-8', 0, 100)}`
        )
        if (!this.requests[requestKey]) {
          return
        }
        if (cmd === 'close') {
          this.requests[requestKey].LANSocket.end()
          this.requests[requestKey] = null
          delete this.requests[requestKey]
        } else if (cmd === 'data') {
          console.log(
            `send b data: ${Buffer.from(data).toString('utf-8', 0, 100)}`
          )
          this.requests[requestKey].LANSocket.write(data)
        }
      })
    }
  }

  /* 根据参数options中的配置{
      local_port: 8090,
      remote_port: 9080
    } 创建本地监听
  */
  createLocalListeningSocket(serverKey, options, unid) {
    if (options.port === undefined) {
      console.error('please set a local port for the local listening socket!')
      return
    }
    if (!unid) {
      console.error('unid 不能为空')
      return
    }
    console.log(`listening the port of ${options.port}`)
    try {
      const activeSocketObj = this.getActiveSocketByKey(unid)
      const serverService = net.Server(async (LANSocket) => {
        if (!this.isConnected) {
          LANSocket.end()
          LANSocket.destroy()
        }
        const requestKey = uid('UR')
        console.log(`new requestKey: ${requestKey} serverKey: ${serverKey}`)
        console.log('ken.length: ', requestKey.length)
        this.requests[requestKey] = { serverKey, LANSocket }
        const data = this.encrypt({ cmd: 'data', requestKey, data: '', unid })
        if (activeSocketObj.activeSocket) {
          activeSocketObj.activeSocket.emit(serverKey, data)
        }
        LANSocket.on('data', (data) => {
          if (!this.isConnected) {
            LANSocket.end()
            LANSocket.destroy()
          }
          // console.log(`data from client 1 : ${data.toString()}`);
          const activeSocketObj = this.getActiveSocketByKey(unid)
          if (activeSocketObj && activeSocketObj.activeSocket) {
            try {
              console.log(
                `send data from client to server: ${Buffer.from(data).toString(
                  'utf-8',
                  0,
                  100
                )}`
              )
              activeSocketObj.activeSocket.emit(
                serverKey,
                this.encrypt({ cmd: 'data', requestKey, data, unid })
              )
            } catch (err) {
              console.error(err)
            }
          }
        })
        LANSocket.on('close', () => {
          console.log(this.unids)
          console.log('连接关闭')
          console.log(activeSocketObj.unid)
          if (activeSocketObj.activeSocket && this.requests[requestKey]) {
            activeSocketObj.activeSocket.emit(
              serverKey,
              this.encrypt({ cmd: 'close', requestKey })
            )
          }
          this.requests[requestKey] = null
          delete this.requests[requestKey]
          LANSocket.end()
          LANSocket.destroy()
        })
        LANSocket.on('error', (err) => {
          LANSocket.end()
          LANSocket.destroy()
          console.log(`${new Date().toLocaleString()}: ${err.message}`)
        })
      })
      serverService.on('error', (args) => {
        console.error(args)
        console.log(
          `用户UUID:${unid}, ${args.port}端口已经被使用了，不能把内网${options.host}:${args.port}暴露到服务器的80端口。`
        )
      })
      serverService.listen(options.port, () => {
        console.log(`NAR serv running at ${options.port}`)
        activeSocketObj.localListeningSockets[serverKey] = serverService
      })

      console.log('断开后必须重新绑定事件')
      this.updateLocalListeningSocket(serverKey, unid)
    } catch (err) {
      console.error('端口已被占用')
      console.error(err)
    }
  }

  updateLocalListeningSocket(serverKey, unid) {
    const activeSocketObj = this.getActiveSocketByKey(unid)
    activeSocketObj.activeSocket.on(serverKey, (d) => {
      d = this.decrypt(d)
      const { cmd, data, requestKey } = d
      if (!this.requests[requestKey]) {
        return
      }
      if (cmd === 'close') {
        this.requests[requestKey].LANSocket.end()
        this.requests[requestKey] = null
        delete this.requests[requestKey]
      } else if (cmd === 'data') {
        console.log(
          `send b data: ${Buffer.from(data).toString('utf-8', 0, 100)}`
        )
        this.requests[requestKey].LANSocket.write(data)
      }
    })
  }

  /*
   * binds: ex:['wall', 'www']
   */
  handleRemoveBinds(data, localListeningSockets) {
    if (Object.prototype.toString.call(data) === '[object Array]') {
      data.forEach((key) => {
        if (localListeningSockets[key]) {
          console.log(key)
          localListeningSockets[key].close()
          localListeningSockets[key] = null
          delete localListeningSockets[key]
        }
      })
    }
  }

  printDataStructure() {
    console.log(
      '\n-----------------------------------begin-----------------------------------------'
    )
    this.activeSocketObjs.forEach((item) => {
      console.log(`active socket key: ${item.unid}`)
      console.log('----------------begin----------------')
      item.localListeningSockets.forEach((i) => {
        console.log(`key: ${i}`)
      })
      console.log('----------------end------------------')
    })
    console.log(
      '----------------------------------end---------------------------------------------\n'
    )
  }
  init(argv) {
    console.log('starting...')
    this.activeSocketObjs = []
    this.natPort = argv.listentPort
    this.server_config = argv
    if (!this.natPort) {
      this.natPort = 27
    }
    this.encryptMethod = argv.encrypt
    if (encryptList.includes(this.encryptMethod)) {
      this.key = argv.key
      this.iv = argv.iv
    }
    this.requests = {}
  }
  // 根据longSocket，从activeSocketObjs移除activeSocketObj
  removeActiveSocketObjByLongSocket(longSocket) {
    for (let i = 0, len = this.activeSocketObjs.length; i < len; i++) {
      if (this.activeSocketObjs[i].activeSocket === longSocket) {
        this.activeSocketObjs.splice(i, 1)
        return
      }
    }
  }
  start() {
    return this.createactiveSocket({ port: this.natPort })
  }
  close() {
    this.activeIO.close()
    this.httpServer.close()
  }
}

function getIP(socket) {
  var ip = null
  if (socket.handshake.headers['x-forwarded-for'] != null) {
    ip = socket.handshake.headers['x-forwarded-for']
  } else {
    ip = socket.handshake.address
  }

  return ip
}

module.exports = { ActiveServer }
